package org.jsequences.io;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import com.google.common.io.BaseEncoding;
import com.google.common.io.ByteSink;
import com.google.common.io.ByteSource;
import com.google.common.io.ByteStreams;


/**
 * A ByteSink implementation which stores the written data internally into a <code>byte[]</code>.
 * This class behaves pretty much like a ByteArrayOutputStream and can be used for the
 * same purpose: To build a resource in memory in a first step and read it in in a second.
 * In this case, the resource to build is a ByteSource out of a ByteSink. Both Sink and Source are
 * "ResourceManagers" for Streams, much like an Iterable is a ResourceManager for an
 * Iterator.
 * <p>
 * After writing with the methods of Sink, the ByteArraySink provides multiple methods to obtain the
 * written data:
 * <ol>
 * <li>Via toByteArray()/toByteSource(): These create truncated copies of the written data which
 * will be independent of this Sink.</li>
 * <li>Via asByteBuffer()/asByteSource(): These create cheap views of the written data which will be
 * sparse (they leak memory). Since views are not independent, write()-Operations may or may not
 * write through to them, see the appropiate javadocs. Try to use them only if you are sure that the
 * sink cannot be changed afterwards!</li>
 * </ol>
 * <p>
 * <b>Performance notes:</b> Handling Streams in memory always yields the risk of OutOfMemory-Errors
 * if the size is unbounded. Therefore, use this class only in scenarios where you are sure the size
 * won't expand beyond limits in the lower megabyte-Range. The big upside of in-Memory-Stream
 * handling however is that it needs absolutely <b>no</b> kind of cleanup and is usually very fast.
 */
public final class ByteArraySink extends ByteSink {

    private ViewableByteArrayOutputStream out;

    /**
     * Creates a new ByteArraySink with the default initial capacity.
     */
    public ByteArraySink() {
        this(64);
    }

    /**
     * Creates a new ByteArraySink, with the specified initial capacity.
     * 
     * @param capacity the initial capacity.
     * @exception IllegalArgumentException if size is negative.
     */
    public ByteArraySink(int capacity) {
        out = new ViewableByteArrayOutputStream(capacity);
    }

    /**
     * Returns the stream to which all data should be written.
     * This method will always return the same stream as long as the data is not consumed.
     */
    @Override
    public OutputStream openStream() {
        return out;
    }

    /**
     * Returns the written data as a hex-encoded String.
     */
    @Override
    public String toString() {
        return out.toString();
    }

    /**
     * Allocates a <i>new</i> array and copies all written bytes into it.
     * 
     * @return the current contents of this sink, as a byte array.
     */
    public byte[] toByteArray() {
        return out.toByteArray();
    }

    /**
     * Creates a <i>new</i> ByteSource and copies a written bytes into it.
     * Later changes to the sink are not reflected.
     * <p>
     * This method should be used whenever a 'safe' and independent copy should be created, for
     * example to store it into a bean or as a return value of a public method. If you just need a
     * 'fast' view which does not involve a copy of the internal data and potentially leaks memory,
     * then consider using asByteSource() instead.
     */
    public ByteSource toByteSource() {
        return ByteStreams.asByteSource(toByteArray());
    }

    /**
     * Returns a readonly buffer view which represents a snapshot of the <b>current</b>
     * data contained in this sink. Usually the view will be sparse and leaks memory,
     * since the internal structures are on average 50% larger than needed. The capacity of
     * the buffer is undefined: its size may only be queried with the limit()-method.
     * <p>
     * <b>Warning</b>: Multiple methods of this class have an undefined behavior: After a write() or
     * reset() operation on the sink, the contents of the buffer are completely <i>undefined</i>! As
     * well, the capacity of the buffer is undefined. Therefore, all methods whic Therefore, this
     * method should be used sparingly and with great care!
     * <p>
     * This method exists just to interoperate smoothly with java's NIO-library. If you just need
     * safe IO view of the internal data then <b>use asByteSource()</b>, which has fewer pitfalls
     * and is easier to use!
     */
    public ByteBuffer asByteBuffer() {
        return out.asByteBuffer();
    }

    /**
     * Views the currently written data as a readonly ByteSource. Usually the view will be sparse
     * and leaks memory, since the internal structures are on average 50% larger than needed.
     * Changes to the underlying sink are reflected to the source.
     * <p>
     * <b>Warning</b>: InputStream of the ByteSource behaves in the same way as the asByteBuffer()
     * view: The data read will be undefined if the sink is modified will it is processed.
     */
    public ByteSource asByteSource() {
        return out.asByteSource();
    }

    /**
     * Drops all data which has been written to this sink and frees up the used memory.
     * Afterwards, the sink behaves as if it was just created.
     */
    public void reset() {
        out.reset();
    }
}

/**
 * An Extension of the ByteArrayOutputStream which allows it to read the contents of
 * the Stream with readonly-views.
 * 
 * @author Eric Giese
 */
final class ViewableByteArrayOutputStream extends ByteArrayOutputStream {

    public ViewableByteArrayOutputStream(int capacity) {
        super(capacity);
    }

    @Override
    public String toString() {
        return BaseEncoding.base16().encode(buf, 0, count);
    }

    /**
     * Views the Stream as a readonly buffer.
     */
    public ByteBuffer asByteBuffer() {
        return ByteBuffer.wrap(buf, 0, count).asReadOnlyBuffer();
    }

    /**
     * Views the Stream as a ByteSource.
     */
    public ByteSource asByteSource() {
        return new AsByteSource();
    }

    private final class AsByteSource extends ByteSource {

        @Override
        public InputStream openStream() throws IOException {
            return new ByteArrayInputStream(buf, 0, count);
        }


        @Override
        public long size() throws IOException {
            return count;
        }

        @Override
        public byte[] read() throws IOException {
            return ViewableByteArrayOutputStream.this.toByteArray();
        }

        @Override
        public long copyTo(OutputStream output) throws IOException {
            output.write(buf, 0, count);
            return count;
        }

        @Override
        public String toString() {
            String seq = ViewableByteArrayOutputStream.this.toString();
            return "ByteArrayOutputStream.asByteSource(" + seq + ')';
        }
    }
}
